defmodule EkangScraper do
  alias EkangScraper.HttpClient, as: HTTP
  alias EkangScraper.Schema.{Report, ReportItem}
  alias EkangScraper.Repo

  @sup EkangScraper.Task.Supervisor

  def login do
    doc = HTTP.get("http://api-backend.ekang99.com:8089/login")
    [{_, attrs, _}] = Floki.find(doc, ~s{input[name="_token"]})
    token = Map.new(attrs)["value"]

    HTTP.post("http://api-backend.ekang99.com:8089/login", %{
      "email" => "29464913@qq.com",
      "password" => "shanghai123",
      "_token" => token
    })
  end

  def reports do
    Stream.cycle([1])
    |> Stream.with_index(1)
    |> Stream.map(&HTTP.get("http://api-backend.ekang99.com:8089/reports", %{"page" => elem(&1, 1)}))
    |> Stream.map(fn doc -> Floki.find(doc, "#report_list tbody tr") end)
    |> Stream.take_while(fn
      [_ | _] -> true
      _ -> false
    end)
    |> Enum.flat_map(&(&1))
  end

  def personal_reports(tr) do
    [{_, attrs, _}] = Floki.find(tr, "td:last-child a")
    url = Map.new(attrs)["href"]
    HTTP.get(url)
    |> Floki.find("#common-list-body tbody tr")
  end

  def report(tr) do
    [{_, attrs, _}] = Floki.find(tr, "td:last-child a")
    url = Map.new(attrs)["href"]
    id = url |> String.split("/") |> Enum.at(-1) |> String.to_integer()
    {id, HTTP.get(url)}
  end

  def insert_report({id, dom}) do
    [{_, _, [text]}] = Floki.find(dom, ".report_name h5")
    name = text |> String.split(": ") |> Enum.at(-1)
    [person_table, _, report_table] = Floki.find(dom, ".table")
    [{_, _, [text]}] = Floki.find(person_table, "tr:first-child>td:first-child")
    gender = text |> String.split(": ") |> Enum.at(-1)
    [{_, _, [text]}] = Floki.find(person_table, "tr:first-child>td:nth-child(2)")
    [_, height, weight] = Regex.run(~r/\((.*),(.*)\)/, text)
    [{_, _, [text]}] = Floki.find(person_table, "tr:nth-child(2)>td:first-child")
    mobile = text |> String.split(": ") |> Enum.at(-1)
    [{_, _, [text]}] = Floki.find(person_table, "tr:nth-child(3)>td:first-child")
    id_no = text |> String.split(": ") |> Enum.at(-1)
    [{_, _, [text]}] = Floki.find(person_table, "tr:nth-child(3)>td:nth-child(2)")
    {:ok, report_time, _} = text |> String.split(": ") |> Enum.at(-1) |> Kernel.<>("+0800") |> DateTime.from_iso8601()

    report = %Report{
      id: id,
      name: name,
      gender: gender,
      height: height,
      weight: weight,
      mobile: mobile,
      id_no: id_no,
      report_time: report_time
    }

    try do
      {:ok, report} = Repo.insert(report)
      insert_report_items(report, report_table)
    rescue
      _ -> nil
    end
  end

  def insert_report_items(report, table) do
    Floki.find(table, "tbody tr")
    |> categorize_report_items(nil, %{})
    |> Map.values()
    |> Enum.flat_map(&(&1))
    |> Enum.reject(&is_nil(&1))
    |> Enum.map(&Task.Supervisor.async(@sup, fn -> Repo.insert(%{&1 | report: report}) end))
    |> Enum.map(&Task.await(&1, 600_000))
  end

  # Done
  defp categorize_report_items([], _, acc), do: acc

  # Head row
  defp categorize_report_items([{_, _, [{"th", _, [category]}]} | rest], _, acc) do
    categorize_report_items(rest, category, Map.put(acc, category, []))
  end

  defp categorize_report_items([tr | rest], category, acc) do
    item = case Floki.find(tr, "td") do
      [
        _, 
        {_, _, [name]},
        {_, _, [range]},
        {_, _, [measured_value]},
        {_, _, [result]}
      ] ->
        range = range |> String.split(~r/\s*-\s*/)
        %ReportItem{
          name: name,
          normal_min: range |> Enum.at(0),
          normal_max: range |> Enum.at(-1),
          measured_value: measured_value,
          result: result,
          genre: category
        }
      ##########################################
      [
        _, 
        {_, _, [name]},
        {_, _, [range]},
        {_, _, [measured_value]}
        | _
      ] ->
        range = range |> String.split(~r/\s*-\s*/)
        %ReportItem{
          name: name,
          normal_min: range |> Enum.at(0),
          normal_max: range |> Enum.at(-1),
          measured_value: measured_value,
          genre: category
        }
        
      ##########################################
      [
        _,
        {_, _, [name]},
        {_, _, [measured_value]}
        | _
      ] ->
        %ReportItem{
          name: name,
          measured_value: measured_value,
          genre: category
        }
      ##########################################
      _ ->
        nil
    end

    categorize_report_items(rest, category, %{acc | category => [item | acc[category]]})
  end

  def scrape do
    login()
    reports()
    |> Stream.map(&Task.Supervisor.async(@sup, fn -> personal_reports(&1) end))
    |> Stream.flat_map(&Task.await(&1, 600_000))
    |> Stream.map(&Task.Supervisor.async(@sup, fn -> report(&1) end))
    |> Stream.map(&Task.await(&1, 600_000))
    |> Enum.map(&Task.Supervisor.async(@sup, fn -> insert_report(&1) end))
    |> Enum.map(&Task.await(&1, 600_000))
  end

  def main(_) do
    scrape()
  end
end
